// --------------------------------------------------------------------------------------------------------------------
// <copyright file="WindowsAzureTraceSwitchProvider.cs" company="AzureContrib (Open source library http://azurecontrib.codeplex.com)">
//   Microsoft Public License (Ms-PL)
// </copyright>
// <summary>
//   Exception for errors when handling Windows Azure Cloud Drives.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

using AzureContrib.WindowsAzure.Diagnostics.Properties;
using AzureContrib.WindowsAzure.ServiceRuntime;
using RoleEnvironmentChangingEventArgs = AzureContrib.WindowsAzure.ServiceRuntime.RoleEnvironmentChangingEventArgs;
using RoleEnvironmentConfigurationSettingChange = AzureContrib.WindowsAzure.ServiceRuntime.RoleEnvironmentConfigurationSettingChange;

namespace AzureContrib.WindowsAzure.Diagnostics
{
	/// <summary>
	/// This <see cref="ITraceSwitchProvider"/> produces <see cref="TraceSwitch"/> instances based on configuration read from the Windows Azure Config.
	/// The configuration setting names are found using <see cref="TraceSwitchKeyBase"/>, <see cref="DefaultTraceSwitchKey"/> and <see cref="TraceSwitchKey"/>.
	/// The format of the value for a trace switch setting is 'switch:{switch}|tracelevel:{tracelevel}[|description:{description}]' or for short 's:{switch}|t:{tracelevel}[|d:{description}]'.
	/// </summary>
	public class WindowsAzureTraceSwitchProvider : ITraceSwitchProvider, IDisposable
	{
		/// <summary>
		/// The separation character for parts of trace switch data.
		/// </summary>
		public const char TraceSwitchWildcard = '*';

		/// <summary>
		/// The separation character for parts of trace switch data.
		/// </summary>
		public const char TraceDataSeparator = '|';

		/// <summary>
		/// The separation character between a key/value trace switch data pair
		/// </summary>
		public const char KeyValueSeparator = ':';

		/// <summary>
		/// The base for a trace switch setting key in Windows Azure Config.
		/// </summary>
		public static readonly string TraceSwitchKeyBase = string.Concat(typeof(WindowsAzureTraceSwitchProvider).FullName, ".Setting.");

		/// <summary>
		/// The Windows Azure configuration key for the default trace switch.
		/// </summary>
		public static readonly string DefaultTraceSwitchKey = string.Concat(TraceSwitchKeyBase, "Default");

		private TraceSwitch defaultTraceSwitch;
		private LogLevel defaultTraceLevel;
		private IDictionary<string, TraceSwitch> traceSwitches;
		private IDictionary<string, TraceSwitch> configuredPatterns;
		private bool disposed;

		/// <summary>
		/// Initializes a new instance of the <see cref="WindowsAzureTraceSwitchProvider"/> class. 
		/// Create an instance of the provider of <see cref="TraceSwitch"/> for Windows Azure.
		/// </summary>
		/// <param name="roleEnvironment">Access to the <see cref="IRoleEnvironment"/>.
		/// </param>
		/// <remarks>The default is <see cref="LogLevel.Warning"/>.</remarks>
		public WindowsAzureTraceSwitchProvider(IRoleEnvironment roleEnvironment)
			: this(roleEnvironment, LogLevel.Warning)
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="WindowsAzureTraceSwitchProvider"/> class. 
		/// Create an instance of the provider of <see cref="TraceSwitch"/> for Windows Azure.
		/// </summary>
		/// <param name="roleEnvironment">Access to the <see cref="IRoleEnvironment"/>.
		/// </param>
		/// <param name="level">The default trace level to use if no config is present.</param>
		/// <remarks>The default is <see cref="LogLevel.Warning"/>.</remarks>
		public WindowsAzureTraceSwitchProvider(IRoleEnvironment roleEnvironment, LogLevel level)
		{
			RoleEnvironment = roleEnvironment;
			RoleEnvironment.Changing += RoleEnvironmentChangingHandler;
			ResetTraceSwitches(level);
		}

		internal IRoleEnvironment RoleEnvironment { get; set; }

		/// <summary>
		/// Use this method to generate the next trace switch setting key for use in Windows Azure config.
		/// </summary>
		/// <param name="index">The index of the key to generate</param>
		/// <returns>A key to use in Windows Azure config for diagnostic trace switch data.</returns>
		public static string TraceSwitchKey(int index)
		{
			if (index < 0)
			{
				throw new ArgumentOutOfRangeException("index", index, Strings.HasToBeZeroOrMore);
			}

			return string.Concat(TraceSwitchKeyBase, index.ToString(CultureInfo.CurrentCulture));
		}

		/// <summary>
		/// Unhooks the <see cref="IRoleEnvironment"/> changing event from this instance.
		/// </summary>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// Find the <see cref="TraceSwitch"/> for this <see cref="ITraceSwitchable"/>?
		/// </summary>
		/// <param name="traceSwitchable">
		/// The trace Switchable.
		/// </param>
		/// <returns>
		/// The <see cref="TraceSwitch"/> to use for the <paramref name="traceSwitchable"/>.
		/// </returns>
		[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "AzureContrib.WindowsAzure.Core.Assumes.NotNull handles null checking.")]
		public TraceSwitch GetSwitch(ITraceSwitchable traceSwitchable)
		{
			Assumes.NotNull(traceSwitchable, "traceSwitchable");

			TraceSwitch value;
			if (traceSwitches.TryGetValue(traceSwitchable.Switch, out value))
			{
				return value;
			}

			var mostVerboseMatch = configuredPatterns.Where(kvp => Regex.IsMatch(traceSwitchable.Switch, kvp.Key))
																							 .Select(kvp => kvp.Value)
																							 .OrderByDescending(ts => ts.Level).FirstOrDefault();
			if (mostVerboseMatch == null)
			{
				mostVerboseMatch = defaultTraceSwitch;
			}

			traceSwitches.Add(traceSwitchable.Switch, mostVerboseMatch);

			return mostVerboseMatch;
		}

		/// <summary>
		/// Check if logging is enabled for <paramref name="traceSwitchable"/> on the <paramref name="logLevel"/>.
		/// </summary>
		/// <param name="logLevel">The level to control logging for.</param>
		/// <param name="traceSwitchable">The <see cref="ITraceSwitchable"/> to check for enablement.</param>
		/// <returns>True if logging is enabled.</returns>
		public bool Enabled(LogLevel logLevel, ITraceSwitchable traceSwitchable)
		{
			return GetSwitch(traceSwitchable).Level <= logLevel;
		}

		internal static TraceSwitch GetTraceSwitch(string traceSwitchData)
		{
			if (string.IsNullOrEmpty(traceSwitchData))
			{
				throw new ArgumentNullException("traceSwitchData", Strings.TraceSwitchDataCannotBeNullOrEmpty);
			}

			var traceSwitchDataParts = traceSwitchData.Split(TraceDataSeparator);

			if (traceSwitchDataParts.Length < 2 || traceSwitchDataParts.Length > 3)
			{
				throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.UnableToParseTheSwitchData, traceSwitchData));
			}

			string displayName = null;
			string description = null;
			string defaultSwitchValue = null;

			var i = 0;
			var propertyNames = new[] { "switch", "loglevel", "description" };
			foreach (var kvp in traceSwitchDataParts.Select(traceSwitchDataPart => traceSwitchDataPart.Split(KeyValueSeparator)))
			{
				var keyValue = kvp;

				if (keyValue.Length == 1)
				{
					keyValue = new[] { propertyNames[i], keyValue[0] };
				}

				switch (keyValue[0].ToUpperInvariant())
				{
					case "SWITCH":
					case "S":
						displayName = keyValue[1];
						break;
					case "LOGLEVEL":
					case "L":
						defaultSwitchValue = keyValue[1];
						break;
					case "DESCRIPTION":
					case "D":
						description = keyValue[1];
						break;
					default:
						throw new ArgumentException(Strings.ExpectedTraceDataFormat);
				}

				i++;
			}

			if (string.IsNullOrEmpty(displayName))
			{
				throw new ArgumentException(Strings.TraceConfigDataMissingSwitch);
			}

			if (string.IsNullOrEmpty(description))
			{
				description = string.Empty;
			}

			if (string.IsNullOrEmpty(defaultSwitchValue))
			{
				throw new ArgumentException(Strings.TraceConfigDataMissingTraceLevel);
			}

			return new TraceSwitch(displayName, description, defaultSwitchValue.ParseLogLevel());
		}

		internal static string CleanRegexPattern(string pattern)
		{
			if (string.IsNullOrEmpty(pattern))
			{
				return pattern;
			}

			// allocate a StringBuilder with some overhead compared to incoming
			// string, which should allow for the regex to expand a bit without
			// need for further allocations
			// var sb = new StringBuilder((int)(pattern.Length * 1.2));
			var sb = new StringBuilder((int)(pattern.Length * 1.5) + 3);

			foreach (var character in pattern)
			{
				switch (character)
				{
					case '.':
						sb.Append(@"\.");
						break;
					case '*':
						sb.Append(".*?");
						break;
					default:
						sb.Append(character);
						break;
				}
			}

			return sb.ToString();

			// implementation with less code, but slower (especially in Release builds)
			// return incomingSwitch
			// .Replace(".", @"\.")
			// .Replace("*", ".*?");
		}

		internal void ResetTraceSwitches(LogLevel traceLevel)
		{
			defaultTraceLevel = traceLevel;

			configuredPatterns = new Dictionary<string, TraceSwitch>();
			traceSwitches = new Dictionary<string, TraceSwitch>();

			// Temp default trace switch
			defaultTraceSwitch = new TraceSwitch(TraceSwitchWildcard.ToString(CultureInfo.InvariantCulture), Strings.TemporaryDefaultTraceSwitch, LogLevel.Verbose);

			// Get the configured trace switches
			var realDefaultTraceSwitch = GetDefaultTraceSwitch(traceLevel);
			var switches = GetConfiguredTraceSwitches();

			// Set the configured trace switches
			defaultTraceSwitch = new TraceSwitch(TraceSwitchWildcard.ToString(CultureInfo.InvariantCulture), realDefaultTraceSwitch.Description, realDefaultTraceSwitch.Level);
			configuredPatterns.Clear();
			configuredPatterns.Add(CleanRegexPattern(defaultTraceSwitch.DisplayName), defaultTraceSwitch);
			traceSwitches.Clear();
			foreach (var traceSwitch in switches)
			{
				configuredPatterns.Add(CleanRegexPattern(traceSwitch.DisplayName), traceSwitch);
				traceSwitches.Add(traceSwitch.DisplayName, traceSwitch);
			}
		}

		internal IEnumerable<TraceSwitch> GetConfiguredTraceSwitches()
		{
			var switches = new List<TraceSwitch>();
			string traceSwitchData;
			int i = 0;
			while (RoleEnvironment.TryGetConfigurationSettingValue(TraceSwitchKey(i++), out traceSwitchData))
			{
				System.Diagnostics.Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.FoundATraceSetting, traceSwitchData), LogLevel.Verbose.ToString());

				var traceSetting = GetTraceSwitch(traceSwitchData);
				switches.Add(traceSetting);
			}

			System.Diagnostics.Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.FoundNumberOfTraceSettings, switches.Count), LogLevel.Verbose.ToString());

			return switches;
		}

		internal TraceSwitch GetDefaultTraceSwitch(LogLevel traceLevel)
		{
			string defaultTraceSwitchData;
			TraceSwitch traceSwitch;
			if (RoleEnvironment.TryGetConfigurationSettingValue(DefaultTraceSwitchKey, out defaultTraceSwitchData))
			{
				System.Diagnostics.Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.FoundADefaultTraceSetting, defaultTraceSwitchData), LogLevel.Verbose.ToString());

				if (defaultTraceSwitchData.Split(TraceDataSeparator).Length == 1)
				{
					defaultTraceSwitchData = string.Format(CultureInfo.InvariantCulture, "{0}|{1}", TraceSwitchWildcard, defaultTraceSwitchData);
				}

				traceSwitch = GetTraceSwitch(defaultTraceSwitchData);
			}
			else
			{
				System.Diagnostics.Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.DidNotFindADefaultTraceSetting, DefaultTraceSwitchKey, traceLevel), LogLevel.Verbose.ToString());
				traceSwitch = new TraceSwitch(TraceSwitchWildcard.ToString(CultureInfo.InvariantCulture), string.Empty, traceLevel);
			}

			return traceSwitch;
		}

		internal void RoleEnvironmentChangingHandler(object sender, RoleEnvironmentChangingEventArgs e)
		{
			if (!e.Changes
				  .Where(rec => rec is RoleEnvironmentConfigurationSettingChange)
				  .Cast<RoleEnvironmentConfigurationSettingChange>()
				  .Any(recsc => recsc.ConfigurationSettingName.StartsWith(TraceSwitchKeyBase, StringComparison.Ordinal)))
			{
				return;
			}

			ResetTraceSwitches(defaultTraceLevel);
		}

		/// <summary>
		/// Release the subscription to <see cref="IRoleEnvironment.Changing"/>
		/// </summary>
		/// <param name="disposing">Are we calling from .Dispose() or from finalizer?</param>
		protected virtual void Dispose(bool disposing)
		{
			if (disposed)
			{
				return;
			}

			if (disposing)
			{
				RoleEnvironment.Changing -= RoleEnvironmentChangingHandler;
			}

			disposed = true;
		}
	}
}